/*
 * Copyright 2011 Katsunori Koyanagi
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */

package sweezy.swf.decoder
{
	import flash.utils.ByteArray;
	import sweezy.swf.action.Action;
	import sweezy.swf.action.ActionInfo;
	import sweezy.swf.action.code.ActionCall;
	import sweezy.swf.action.code.ActionConstantPool;
	import sweezy.swf.action.code.ActionDefineFunction;
	import sweezy.swf.action.code.ActionDefineFunction2;
	import sweezy.swf.action.code.ActionGetURL;
	import sweezy.swf.action.code.ActionGetURL2;
	import sweezy.swf.action.code.ActionGoToLabel;
	import sweezy.swf.action.code.ActionGotoFrame;
	import sweezy.swf.action.code.ActionGotoFrame2;
	import sweezy.swf.action.code.ActionIf;
	import sweezy.swf.action.code.ActionJump;
	import sweezy.swf.action.code.ActionPush;
	import sweezy.swf.action.code.ActionPushValue;
	import sweezy.swf.action.code.ActionSetTarget;
	import sweezy.swf.action.code.ActionStoreRegister;
	import sweezy.swf.action.code.ActionTry;
	import sweezy.swf.action.code.ActionUnknown;
	import sweezy.swf.action.code.ActionWaitForFrame;
	import sweezy.swf.action.code.ActionWaitForFrame2;
	import sweezy.swf.action.code.ActionWith;
	import sweezy.swf.constant.ActionPushType;
	import sweezy.swf.constant.SWFVersion;
	import sweezy.swf.io.SWFInput;
	import sweezy.swf.structure.RegisterParam;

	internal class ActionDecoder
	{

		private var _in:SWFInput;

		private var _version:uint;

		public function ActionDecoder()
		{
		}

		public function decode(version:uint, bytes:ByteArray):Array
		{
			_version = version;
			_in = new SWFInput(bytes);

			var records:Array = decodeActionRecords0(bytes.length);

			_in = null;
			return records;
		}

		private function decodeActionCall(length:uint):Action
		{
			return new ActionCall();
		}

		private function decodeActionConstantPool(length:uint):Action
		{
			var action:ActionConstantPool = new ActionConstantPool();
			var count:uint = _in.readUI16();
			var data:Array = action.constantPool = [];

			for (var i:int = 0; i < count; i++)
			{
				data[i] = decodeString();
			}

			return action;
		}

		private function decodeActionDefineFunction(length:uint):Action
		{
			var action:ActionDefineFunction = new ActionDefineFunction();
			action.name = decodeString();

			var numParams:uint = _in.readUI16();
			var data:Array = action.params = [];
			for (var i:int = 0; i < numParams; i++)
			{
				data[i] = decodeString();
			}

			action.actions = decodeActionRecords0(_in.readUI16());

			return action;
		}

		private function decodeActionDefineFunction2(length:uint):Action
		{
			var action:ActionDefineFunction2 = new ActionDefineFunction2();
			action.functionName = decodeString();
			var numParams:uint = _in.readUI16();
			action.registerCount = _in.readUI8();
			action.preloadParent = _in.readFLAG();
			action.preloadRoot = _in.readFLAG();
			action.suppressSuper = _in.readFLAG();
			action.preloadSuper = _in.readFLAG();
			action.suppressArguments = _in.readFLAG();
			action.preloadArguments = _in.readFLAG();
			action.suppressThis = _in.readFLAG();
			action.preloadThis = _in.readFLAG();
			_in.readUB(7);
			action.preloadGlobal = _in.readFLAG();

			var data:Array = action.parameters = [];
			for (var i:int = 0; i < numParams; i++)
			{
				data[i] = decodeRegisterParam();
			}
			action.actions = decodeActionRecords0(_in.readUI16());

			return action;
		}

		private function decodeActionGetURL(length:uint):Action
		{
			var action:ActionGetURL = new ActionGetURL();
			action.urlString = decodeString();
			action.targetString = decodeString();

			return action;
		}

		private function decodeActionGetURL2(length:uint):Action
		{
			var action:ActionGetURL2 = new ActionGetURL2();
			action.sendVarsMethod = _in.readUB(2);
			_in.readUB(4);
			action.loadTarget = _in.readFLAG();
			action.loadVariables = _in.readFLAG();

			return action;
		}

		private function decodeActionGotoFrame(length:uint):Action
		{
			var action:ActionGotoFrame = new ActionGotoFrame();
			action.frame = _in.readUI16();

			return action;
		}

		private function decodeActionGotoFrame2(length:uint):Action
		{
			var action:ActionGotoFrame2 = new ActionGotoFrame2();
			_in.readUB(6);
			action.hasSceneBias = _in.readFLAG();
			action.play = _in.readFLAG();
			if (action.hasSceneBias)
			{
				action.sceneBias = _in.readUI16();
			}

			return action;
		}

		private function decodeActionGotoLabel(length:uint):Action
		{
			var action:ActionGoToLabel = new ActionGoToLabel();
			action.label = decodeString();

			return action;
		}

		private function decodeActionIf(length:uint):Action
		{
			var action:ActionIf = new ActionIf();
			action.branchOffset = _in.readSI16();

			return action;
		}

		private function decodeActionJump(length:uint):Action
		{
			var action:ActionJump = new ActionJump();
			action.branchOffset = _in.readSI16();

			return action;
		}

		private function decodeActionPush(length:uint):Action
		{
			var action:ActionPush = new ActionPush();
			var values:Array = action.values = [];
			var idx:int = 0;
			var pos:uint = _in.position;

			while (true)
			{
				var value:ActionPushValue = new ActionPushValue();
				value.type = _in.readUI8();
				values[idx++] = value;

				switch (value.type)
				{
					case ActionPushType.STRING:
						value.string = decodeString();
						break;
					case ActionPushType.FLOAT:
						value.float = _in.readFLOAT32();
						break;
					case ActionPushType.REGISTER_NUMBER:
						value.registerNumber = _in.readUI8();
						break;
					case ActionPushType.BOOLEAN:
						value.boolean = _in.readUI8() !== 0;
						break;
					case ActionPushType.DOUBLE:
						value.double = _in.readDOUBLE();
						break;
					case ActionPushType.INTEGER:
						value.integer = _in.readUI32();
						break;
					case ActionPushType.CONSTANT_8:
						value.constant8 = _in.readUI8();
						break;
					case ActionPushType.CONSTANT_16:
						value.constant16 = _in.readUI16();
						break;
				}

				if (length <= _in.position - pos)
				{
					break;
				}
			}

			return action;
		}

		private function decodeActionRecords0(limit:uint):Array
		{
			var result:Array = [];
			var action:Action;
			var idx:uint;
			var pos:uint = _in.position;

			while (true)
			{
				if (_in.position - pos >= limit)
				{
					break;
				}

				var code:uint = _in.readUI8();
				var length:uint = 0;
				if (code >= 0x80)
				{
					length = _in.readUI16();
				}

				var info:ActionInfo = ActionInfo.getActionInfoByCode(code);
				if (info === null)
				{
					action = decodeActionUnknown(code, length);
				}
				else if (code < 0x80)
				{
					action = new info.actionClass();
				}
				else
				{
					var fn:Function;
					switch (code)
					{
						case ActionInfo.ACTION_CALL:
							fn = decodeActionCall;
							break;
						case ActionInfo.ACTION_CONSTANT_POOL:
							fn = decodeActionConstantPool;
							break;
						case ActionInfo.ACTION_DEFINE_FUNCTION:
							fn = decodeActionDefineFunction;
							break;
						case ActionInfo.ACTION_GET_URL:
							fn = decodeActionGetURL;
							break;
						case ActionInfo.ACTION_GOTO_FRAME:
							fn = decodeActionGotoFrame;
							break;
						case ActionInfo.ACTION_GO_TO_LABEL:
							fn = decodeActionGotoLabel;
							break;
						case ActionInfo.ACTION_WAIT_FOR_FRAME:
							fn = decodeActionWaitForFrame;
							break;
						case ActionInfo.ACTION_SET_TARGET:
							fn = decodeActionSetTarget;
							break;
						case ActionInfo.ACTION_IF:
							fn = decodeActionIf;
							break;
						case ActionInfo.ACTION_JUMP:
							fn = decodeActionJump;
							break;
						case ActionInfo.ACTION_WITH:
							fn = decodeActionWith;
							break;
						case ActionInfo.ACTION_TRY:
							fn = decodeActionTry;
							break;
						case ActionInfo.ACTION_PUSH:
							fn = decodeActionPush;
							break;
						case ActionInfo.ACTION_STORE_REGISTER:
							fn = decodeActionStoreRegister;
							break;
						case ActionInfo.ACTION_GOTO_FRAME_2:
							fn = decodeActionGotoFrame2;
							break;
						case ActionInfo.ACTION_WAIT_FOR_FRAME_2:
							fn = decodeActionWaitForFrame2;
							break;
						case ActionInfo.ACTION_GET_URL_2:
							fn = decodeActionGetURL2;
							break;
						case ActionInfo.ACTION_DEFINE_FUNCTION_2:
							fn = decodeActionDefineFunction2;
							break;
						default:
							fn = null;
					}

					if (fn === null)
					{
						action = new info.actionClass();
						_in.skip(length);
					}
					else
					{
						action = fn(length);
					}
				}

				result[idx++] = action;
			}

			return result;
		}

		private function decodeActionSetTarget(length:uint):Action
		{
			var action:ActionSetTarget = new ActionSetTarget();
			action.targetName = decodeString();

			return action;
		}

		private function decodeActionStoreRegister(length:uint):Action
		{
			var action:ActionStoreRegister = new ActionStoreRegister();
			action.registerNumber = _in.readUI8();

			return action;
		}

		private function decodeActionTry(length:uint):Action
		{
			var action:ActionTry = new ActionTry();
			_in.readUB(5);
			action.catchInRegister = _in.readFLAG();
			action.finallyBlockFlag = _in.readFLAG();
			action.catchBlockFlag = _in.readFLAG();

			var trySize:uint = _in.readUI16();
			var catchSize:uint = _in.readUI16();
			var finallySize:uint = _in.readUI16();

			if (action.catchInRegister)
			{
				action.catchRegister = _in.readUI8();
			}
			else
			{
				action.catchName = decodeString();
			}
			action.tryBody = decodeActionRecords0(trySize);
			action.catchBody = decodeActionRecords0(catchSize);
			action.finallyBody = decodeActionRecords0(finallySize);

			return action;
		}

		private function decodeActionUnknown(code:uint, length:uint):Action
		{
			var action:ActionUnknown = new ActionUnknown(code);
			action.operands = _in.readBytes(length);

			return action;
		}

		private function decodeActionWaitForFrame(length:uint):Action
		{
			var action:ActionWaitForFrame = new ActionWaitForFrame();
			action.frame = _in.readUI16();
			action.skipCount = _in.readUI8();

			return action;
		}

		private function decodeActionWaitForFrame2(length:uint):Action
		{
			var action:ActionWaitForFrame2 = new ActionWaitForFrame2();
			action.skipCount = _in.readUI8();

			return action;
		}

		private function decodeActionWith(length:uint):Action
		{
			var action:ActionWith = new ActionWith();
			action.actions = decodeActionRecords0(_in.readUI16());

			return action;
		}

		private function decodeRegisterParam():RegisterParam
		{
			var registerParam:RegisterParam = new RegisterParam();
			registerParam.regster = _in.readUI8();
			registerParam.paramName = decodeString();

			return registerParam;
		}

		private function decodeString(length:uint = 0):String
		{
			if (_version <= SWFVersion.VERSION_5)
			{
				return _in.readString(length, "shift-jis");
			}

			return _in.readString(length);
		}
	}
}
